#include "qe_sdtrace.h"
#include "qe_log.h"
#include "qe_buffer.h"
#include "qe_queue.h"
#include "qe_string.h"
#include "qe_memory.h"
#include "qe_assert.h"



QELOG_DOMAIN(QELOG_DOMAIN_SDTRACE);



#define SDTRACE_VERSION_MAJOR   (1)
#define SDTRACE_VERSION_MINOR   (1)
#define SDTRACE_VERSION_PATCH   (1)

#define SDTRACE_START_MARK      "sdtrace-start"
#define SDTRACE_END_MARK        "sdtrace-end"



static void meta_init(qe_sdtrace_meta *meta)
{
    /* Start mark ='sdtrace-strat' */
    qe_sprintf(meta->start_mark, SDTRACE_START_MARK);

    /* Version */
    meta->version = qe_version_encode(SDTRACE_VERSION_MAJOR,
        SDTRACE_VERSION_MINOR, SDTRACE_VERSION_PATCH);

    meta->size                   = 0;
    meta->num_datas              = 0;
    meta->num_recorded_blocks    = 0;
    meta->max_datas              = 0;
    meta->num_blocks             = 0;
    meta->block_size             = 0;
    meta->num_blocks_align_datas = 0;
    meta->serialize_count        = 0;
    meta->next_free              = 0;
    meta->is_full                = 0;
    meta->is_active              = 0;
    meta->data_start_mark        = 0x44464F53;
    meta->data_end_mark          = 0x44464F45;

    /* End mark ='sdtrace-end' */
    qe_sprintf(meta->end_mark, SDTRACE_END_MARK);
}

static qe_ret meta_refresh(qe_sdtrace *trace)
{
    qe_ret ret;
    qe_sdtrace_blockio *bio = trace->bio;

    qe_debug("erase meta block");
    ret = bio->block_erase(bio, bio->meta_block);
    if (ret != qe_ok) {
        qe_error("meta block erase error:%d", ret);
        return ret;
    }

    qe_debug("write meta chunk");
    ret = bio->chunk_write(bio, bio->meta_chunk, &trace->meta, trace->meta_size);
    if (ret != qe_ok) {
        qe_error("meta chunk write error:%d", ret);
        return ret;
    }

    return qe_ok;
}

static qe_ret blockio_record(qe_sdtrace *trace, qe_const_ptr data, qe_uint num)
{
    qe_u8 *pos;
    qe_int i;
    qe_ret ret;
    qe_u32 block;
    qe_u32 chunk;
    qe_u32 num_chunks;
    qe_u32 data_bytes;
    qe_u32 write_bytes;
    qe_sdtrace_meta *meta = &trace->meta;
    qe_sdtrace_blockio *bio = trace->bio;
    qe_sdtrace_blockio_config *cfg = &bio->config;
    
    pos   = (qe_u8 *)data;
    block =  meta->next_free + bio->start_block;
    chunk = block * cfg->chunks_per_block;

    /* erase blocks */
    for (i=0; i<bio->num_cache_blocks; i++) {
        qe_debug("erase block %d", block);
        ret = bio->block_erase(bio, block);
        if (ret != qe_ok) {
            qe_error("erase block %d error:%d", block, ret);
            return ret;
        }
        block++;
    }

    data_bytes = trace->data_size * num;

    /* write chunks */
    for (i=0; i<bio->num_cache_chunks; i++) {
        write_bytes = qe_min(data_bytes, cfg->chunk_size);
        qe_debug("write chunk %d %d", chunk, write_bytes);
        ret = bio->chunk_write(bio, chunk, pos, write_bytes);
        if (ret != qe_ok) {
            qe_error("write chunk %d error:%d", chunk, ret);
            return ret;
        }
        pos += write_bytes;
        data_bytes -= write_bytes;
        chunk++;
    }

    meta->next_free = (meta->next_free + bio->num_cache_blocks) % bio->num_data_blocks;
    qe_debug("update next free %d", meta->next_free);

    /* check full */
    if (!meta->is_full) {
        meta->num_datas += num;
        meta->num_recorded_blocks += bio->num_cache_blocks;
        if (meta->num_datas >= meta->max_datas) {
            meta->num_datas = meta->max_datas;
            meta->is_full = 1;
            qe_info("record full");
        }
    }

    if (!meta->is_active) {
        meta->is_active = 1;
        qe_debug("record active");
    }

    /* update meta block */
    return meta_refresh(trace);
}

static qe_ret blockio_set(qe_sdtrace *trace, qe_sdtrace_blockio *bio)
{
    qe_ret ret;
    qe_u32 block_size;
    qe_u32 num_data_blocks;
    qe_u32 num_cache_datas;
    qe_u32 total_blocks;
    qe_sdtrace_meta *meta;
    qe_sdtrace_blockio_config *cfg;

    qe_assert(trace != QE_NULL);
    qe_assert(bio != QE_NULL);

    meta = &trace->meta;

    /* Check io functions */
    if (!bio->init || !bio->block_erase || !bio->chunk_read || !bio->chunk_write) {
        qe_error("block io func error");
        return qe_err_param;
    }

    /* Check config params */
    cfg = &bio->config;
    if (!cfg->chunk_size || !cfg->chunks_per_block || !cfg->num_cache_blocks) {
        qe_error("blockio config error");
        return qe_err_param;
    }

    /* Check start block and end block */
    if (cfg->end_block <= cfg->start_block) {
        qe_error("end block %d <= start block %d", cfg->end_block, 
            cfg->start_block);
        return qe_err_param;
    }

    total_blocks = cfg->end_block - cfg->start_block + 1;

    /* Check cache blocks range */
    num_data_blocks = total_blocks - 1;    // one for meta
    if (cfg->num_cache_blocks > num_data_blocks) {
        qe_error("cache blocks %d > data blocks %d", cfg->num_cache_blocks, 
            num_data_blocks);
        return qe_err_param;
    }

    /* Data blocks align to cache blocks */
    if (num_data_blocks % cfg->num_cache_blocks) {
        num_data_blocks = num_data_blocks / cfg->num_cache_blocks * cfg->num_cache_blocks;
        qe_debug("data blocks align %d", num_data_blocks);
    }

    /* Calcalute cache datas */
    block_size = cfg->chunk_size * cfg->chunks_per_block;
    num_cache_datas = (cfg->num_cache_blocks * block_size) / trace->data_size;
    if (!num_cache_datas) {
        qe_error("data size and block size not match");
        return qe_err_match;
    }

    bio->chunk_size              = cfg->chunk_size;
    bio->block_size              = block_size;
    bio->num_blocks              = 1 + num_data_blocks;
    bio->start_block             = cfg->start_block;
    bio->end_block               = cfg->start_block + bio->num_blocks;
    bio->meta_block              = bio->start_block;
    bio->meta_chunk              = bio->meta_block * cfg->chunks_per_block;
    trace->bio                   = bio;
    bio->num_data_blocks         = num_data_blocks;
    bio->num_unused_blocks       = total_blocks - bio->num_blocks;
    bio->num_cache_datas         = num_cache_datas;
    bio->num_cache_blocks        = cfg->num_cache_blocks;
    bio->num_cache_chunks        = bio->num_cache_blocks * cfg->chunks_per_block;

    /* Call blockio init */
    ret = bio->init(bio);
    if (ret != qe_ok) {
        qe_error("blockio init error:%d", ret);
        return ret;
    }

    /* Alloc mem for cache blocks */
    trace->ring = qe_ringq_create(trace->data_size, num_cache_datas);
    if (!trace->ring) {
        qe_error("alloc mem for cache blocks failed");
        return qe_err_mem;
    }

    meta->block_size             = block_size;
    meta->num_blocks             = num_data_blocks + 1;
    meta->num_blocks_align_datas = bio->num_cache_blocks;
    meta->max_datas              = num_cache_datas * num_data_blocks / cfg->num_cache_blocks;
    meta->size                   = (num_data_blocks + 1) * block_size;
    meta->next_free              = 1;

    // ret = meta_refresh(trace);
    // if (ret != qe_ok) {
    //     qe_error("meta refresh error:%d", ret);
    //     qe_free(trace->ring);
    //     return ret;
    // }

    qe_debug("set blockio success");

    return qe_ok;
}

qe_ret qe_sdtrace_load(qe_sdtrace *trace)
{
    char *start_pos;
    qe_ret ret;
    qe_int n;
    qe_uint length;
    qe_uint read_size;
    qe_uint num_occupied_blocks;
    qe_sdtrace_meta *meta, readmeta;
    qe_sdtrace_blockio *bio;

    if (!trace) {
        qe_error("trace null");
        return qe_err_param;
    }

    if (!trace->is_initialized) {
        qe_error("trace not init");
        return qe_err_param;
    }

    meta = &trace->meta;
    bio = trace->bio;

    qe_debug("read meta chunk");
    ret = bio->chunk_read(bio, bio->meta_chunk, &readmeta, trace->meta_size);
    if (ret != qe_ok) {
        qe_error("read meta chunk error:%d", ret);
        return ret;
    }

    if (qe_strcmp(readmeta.start_mark, meta->start_mark) != 0) {
        qe_warning("start mark not match");
        qe_hexdump_warning(readmeta.start_mark, sizeof(readmeta.start_mark));
        return qe_err_match;
    }

    if (readmeta.version != meta->version) {
        qe_warning("version not same, read:%d.%d.%d work:%d.%d.%d",
            (readmeta.version & 0x00FF0000) >> 16,
            (readmeta.version & 0x0000FF00) >> 8,
            (readmeta.version & 0x000000FF), 
            (meta->version & 0x00FF0000) >> 16,
            (meta->version & 0x0000FF00) >> 8,
            (meta->version & 0x000000FF));
        return qe_err_match;
    }

    if (readmeta.size != meta->size) {
        qe_error("size not same, read:%d work:%d", 
            readmeta.size, meta->size);
        return qe_err_match;
    }

    if (readmeta.max_datas != meta->max_datas) {
        qe_error("max not same %d, read:%d work:%d", 
            readmeta.max_datas, meta->max_datas);
        return qe_err_match;
    }

    /* Update meta */
    meta->num_datas = readmeta.num_datas;
    meta->num_recorded_blocks = readmeta.num_recorded_blocks;
    meta->next_free = readmeta.next_free;
    meta->serialize_count = readmeta.serialize_count + 1;
    meta->is_full = readmeta.is_full;
    meta->is_active = readmeta.is_active;

    /* Calculate read size */
    num_occupied_blocks = 1 + meta->num_recorded_blocks;
    read_size = num_occupied_blocks * meta->block_size;

    qe_info("trace load success, meta info:");
    qe_info("serialize count : %d", meta->serialize_count);
    qe_info("is full         : %d", meta->is_full);
    qe_info("is active       : %d", meta->is_active);
    qe_info("next free       : %d", meta->next_free);
    qe_info("num datas       : %d", meta->num_datas);
    qe_info("recorded blocks : %d", meta->num_recorded_blocks);
    qe_info("read size       : %d", read_size);

    return qe_ok;
}

/**
 * @brief Record datas
 * 
 * @param[in] trace: SDTrace
 * @param[in] data: data pointer
 * @param[in] num: number of datas
 * 
 * @return qe_ret 
 */
qe_ret qe_sdtrace_record_datas(qe_sdtrace *trace, qe_const_ptr data, qe_uint num)
{
    qe_u8 *pos;
    qe_ret ret;
    qe_uint free;
    qe_uint num_push;
    qe_sdtrace_meta *meta;

    if (!trace || !data || !num) {
        qe_error("invalid params");
        return qe_err_param;
    }

    if (!trace->is_initialized) {
        qe_error("not initialized");
        return qe_err_param;
    }

    pos = (qe_u8 *)data;
    meta = &trace->meta;

    while (num) {

        /* push data into cache */
        free = qe_ringq_free(trace->ring);
        num_push = qe_min(free, num);
        qe_ringq_enq(trace->ring, pos, num_push);

        pos += trace->data_size * num_push;
        num -= num_push;

        /* cache full trigger blockio update */
        if (qe_ringq_isfull(trace->ring)) {
            qe_info("cache full, trigger blockio record");
            ret = blockio_record(trace, 
                qe_ringq_buf_pos(trace->ring),
                qe_ringq_wait(trace->ring));
            if (ret != qe_ok) {
                qe_error("blockio record error:%d", ret);
                return ret;
            }
            /* don't forget to clear cache */
            qe_ringq_clear(trace->ring);
        }
    }

    return qe_ok;
}

/**
 * @brief Create a sdrace with give size and blockio
 * 
 * @param[in] size: item size in bytes
 * @param[in] bio: blockio
 * 
 * @return qe_sdtrace
 */
qe_sdtrace *qe_sdtrace_new(qe_uint size, qe_sdtrace_blockio *bio)
{
    qe_ret ret;
    qe_sdtrace *trace;

    if (!size || !bio) {
        qe_error("invalid params");
        return QE_NULL;
    }

    /* Create trace */
    trace = qe_malloc(sizeof(qe_sdtrace));
    if (!trace) {
        qe_error("alloc mem for trace failed");
        return QE_NULL;
    }
    qe_memset(trace, 0, sizeof(qe_sdtrace));

    /* Initialize meta */
    meta_init(&trace->meta);

    trace->data_size = size;

    ret = blockio_set(trace, bio);
    if (ret != qe_ok) {
        qe_error("set blockio error:%d", ret);
        qe_free(trace);
        return QE_NULL;
    }

    trace->meta_size = sizeof(trace->meta) - sizeof(trace->meta.end_mark);
    trace->is_initialized = 1;

    qe_info("sdtrace create success");
    qe_info("version         : v%d.%d.%d",
        SDTRACE_VERSION_MAJOR,
        SDTRACE_VERSION_MINOR,
        SDTRACE_VERSION_PATCH);
    qe_info("meta size       : %d", trace->meta_size);
    qe_info("data size       : %d", trace->data_size);
    qe_info("max datas       : %d", trace->meta.max_datas);
    qe_info("block size      : %d", bio->block_size);
    qe_info("using blocks    : %d + %d", 1, bio->num_data_blocks);
    qe_info("unused blocks   : %d", bio->num_unused_blocks);
    qe_info("cache blocks    : %d", bio->num_cache_blocks);
    qe_info("cache datas     : %d", bio->num_cache_datas);
    qe_info("cache size      : %d", bio->num_cache_datas * trace->data_size);
    qe_info("total size      : %d", trace->meta.size);

    return trace;
}

void qe_sdtrace_destroy(qe_sdtrace *trace)
{
    if (!trace || !trace->is_initialized)
        return;

    if (trace->ring)
        qe_free(trace->ring);
    
    qe_free(trace);
}